一、请求参数
为了追求单一职责原则,上篇博客将 ImageLoader 类拆出 3 个类,这种情况往往导致方法调用层次加深,参数传递多次。比如:
其中,参数 url 和 imageView 就传递了多次。如果后期需求变更,要新增参数,比如新增一个回调监听器 listener,那么整个方法调用链上的代码都要大幅修改。这时,我们一般将多个参数封装在一个对象里,这样就可以减少很多修改操作。此处。我们将参数 url 和 imageView 封装在请求参数 BitmapRequest 类中。
对应的方法调用链的参数也需要替换成 BitmapRequest,如 ImageLoader 的 displayImage 方法:
ImageLoader 将用户传递的各种参数封装为 BitmapRequest 对象并沿着方法调用依次传递。为了保证程序的一致性,BitmapRequest 也作为缓存的 key。ImageCache 代码更改如下。
避免啰嗦,就不再给出其他参数修改代码。
二、请求队列
在 RequestThreadPool 类中,我们使用了 ExecutorService.submit(new Runnable(){}) 方法。虽然使用了线程池,支持多个异步加载任务,但是我们无法控制它。比如取消加载任务,修改加载顺序等,而且我们这样的异步加载代码显得非常“丑陋”,阅读性差。具体代码如下所示:
有没有更好的实现方式呢?当然有,而且 ANDROID 本身就提供了一个很棒的设计案例,那就是 Handler。那 Handler 的机制是什么样的呢?先来看下图。
在使用 Handler 的时候,在 Handler 所创建的线程需要维护一个唯一的 Looper 对象,每个线程对应一个 Looper,每个线程的 Looper 通过 ThreadLocal 来保证;Looper 对象的内部又维护有唯一的一个 MessageQueue,所以一个线程可以有多个 Handler,但是只能有一个 Looper 和一个 MessageQueue。
Message 在 MessageQueue 不是通过一个列表来存储的,而是将传入的 Message 存入到了上一个 Message 的 next 中,在取出的时候通过顶部的 Message 就能按放入的顺序依次取出 Message。
Looper 对象通过 loop() 方法开启了一个死循环,不断地从 looper 内的 MessageQueue 中取出 Message,然后通过 handler 将消息分发传回 handler 所在的线程。
阐述完 Handler 消息机制,接下来就是模仿它实现请求队列。如下图所示:
- 首先将 RequestThreadPool 改名为 RequestQueue,相当于 Looper,其内部维护一个存储图片加载请求(BitmapRequest)的队列(LinkedBlockingQueue),向外部提供一个添加图片加载请求的接口(addRequest);
- RequestQueue 再维护一个线程池(RequestDispatcher[]),数量默认为:CPU 核心数 + 1个分发线程数,其中每个线程 RequestDispatcher 相当于 Handler,不断地从请求队列中获取 BitmapRequest,并执行加载任务。
- 实现 RequestDispatcher 类,在相应的条件下无限轮询请求队列(LinkedBlockingQueue),拿到具体图片加载请求(BitmapRequest)后去执行网络加载任务。
实现代码如下:
RequestDispatcher.java
RequestDispatcher 是一个线程实现类,主要负责轮询请求队列,获取到实际请求后,调用 Loader 实例去加载网络图片。
RequestQueue.java
RequestQueue 维护一个请求队列和线程池,在添加加载请求之前必须调用 start 方法来初始化线程池并启动各个线程。
ImageLoader.java
测试代码:
三、请求策略
在上面实现的代码中,加载请求会被封装成一个 Request 对象添加到请求队列中,默认情况下 ImageLoader 会按照先后顺序加载图片。但是现实中,我们可能需要最后添加到队列的请求先被执行。例如,在滚动 ListView 时,最后一项肯定是最晚被加载的,此时它却显示在屏幕上的,而其它优先被加载的请求却不在屏幕显示范围。当需求是在屏幕上显示的 Item View 的图片优先被加载,我们就需要 ImageLoader 支持从请求队列的尾部开始加载。也就是,这里至少需要两种策略。
依照策略模式,代码实现如下所示。
首先定义了一个 LoadPolicy 接口,在这个接口中有一个 compare 方法,用来对比两个请求。我们默认实现了顺序加载、逆序加载两个策略,因为每个请求都要有一个序列号,这个序列号以递增的形式增长,越晚加入队列的请求序列号越大,而我们的请求队列也必须是优先级队列;为实现对这些请求的排序处理,我们需要在图片加载请求类中实现 Comparable 接口。
使用 AtomicInteger 类给每一个请求分配一个序列号,再使用优先级队列(PriorityBlockingQueue)来维持图片加载队列,PriorityBlockingQueue 会根据 BitmapRequest 的 compare 策略来决定 BitmapRequest 的顺序。RequestQueue 内部会启动用户指定数量的线程来从请求队列中读取请求,分发线程不断地从队列中读取请求,然后进行图片加载处理。代码如下:
BitmapRequest 增加请求序列号(serialNum)和加载策略(mLoadPolicy),并实现 Comparable 接口中的 compareTo 方法。
向外部提供设置请求加载接口:
我们在 init 方法中初始化了加载策略配置信息,后续我们额外提供配置参数,如加载中的图片、加载失败的图片、缓存策略等等,那么要重载多少个 init 方法?如果提供 setter 和 getter 方法,那么这些方法是不是都在 displayImage 方法调用之前调用?
四、配置参数
前文提到 ImageLoader 初始化配置参数是,无论是重载 init 方法还是使用多个 setter 方法,都会使得用户的使用成本很高。暴露过多函数,会让用户在每次调用函数时都要仔细选择,还要把握函数调用时机。比如在已经初始化了一个指定线程数量的线程池的情况下,用户再调用 setThreadCount 时应该如何处理呢?为此我们需要对程序做一些限制,让用户只能在初始化时配置这些参数。
要封装参数,还要考虑初始化顺序等问题,只像 BitmapRequest 那样简单的封装参数是不行的。我们可以使用 Builder 模式来构建一个不可变的配置对象,并且将这个对象注入到 ImageLoader 中,也就是说它只能在构建时设置各个参数,一旦你调用 build() 或者类似方法构建对象后,它的属性就不可再修改,因为它没有 setter 方法,字段也都是隐藏的,用户只能在初始化时一次性构建这个配置对象,然后注入给 ImageLoader,ImageLoader 根据配置对象进行初始化。这样,像 setThreadCount、setLoadPolicy 等方法就不需要出现在 ImageLoader 中了,用户可见的函数就会少很多,ImageLoader 的使用成本也随之降低了。
修改后的 ImageLoader,其代码如下。
其中的参数设置代码都封装到了 ImageLoaderConfig 和 Builder 对象中,代码如下:
通过 ImageLoaderConfig 的构造函数、字段包级私有化,使得外部不能访问内部属性,用户唯一能够设置属性的地方就是通过 Builder 对象了,也就是说用户只能通过 Builder 对象构造 ImageLoaderConfig 对象,这就是构建和表示相分离。
用户的使用代码如下所示: